package edu.lehigh.cse.ale;

import org.andengine.engine.camera.SmoothCamera;
import org.andengine.engine.camera.hud.HUD;
import org.andengine.engine.options.EngineOptions;
import org.andengine.engine.options.resolutionpolicy.RatioResolutionPolicy;
import org.andengine.entity.scene.IOnSceneTouchListener;
import org.andengine.entity.scene.Scene;
import org.andengine.input.sensor.acceleration.AccelerationData;
import org.andengine.input.sensor.acceleration.IAccelerationListener;
import org.andengine.input.touch.TouchEvent;
import org.andengine.ui.activity.SimpleBaseGameActivity;
import org.andengine.util.debug.Debug;

import android.app.Dialog;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
import com.badlogic.gdx.physics.box2d.Contact;
import com.badlogic.gdx.physics.box2d.ContactImpulse;
import com.badlogic.gdx.physics.box2d.ContactListener;
import com.badlogic.gdx.physics.box2d.Manifold;
import com.badlogic.gdx.physics.box2d.WorldManifold;
import com.example.mygame.R;

/**
 * Every game must extend this Framework class to gain access to all of the components of ALE. See ALEDemoGame for an
 * example.
 */
abstract public class ALE extends SimpleBaseGameActivity implements IAccelerationListener, ContactListener, IOnSceneTouchListener
{
    /*
     * BASIC FUNCTIONALITY
     */

    /**
     * The camera for this game
     * 
     * NB: The only reason we keep this around as a member is to avoid casting it to a SmoothCamera whenever we use it
     */
    SmoothCamera _camera;

    /**
     * A reference to the currently active ALE Game
     */
    static ALE   _self;

    /**
     * Track if the level-picker and menu system are configured
     */
    boolean      _menuManagerConfigured;

    /**
     * The help scene is managed through this
     */
    HelpScene    _helpScene;

    /**
     * Since the enable/disable accelerometer methods are protected, and we want to call them from another class, we
     * need this helper
     * 
     * Note: user code should never call this
     * 
     * @param active
     *            True if the accelerometer should be turned on
     */
    void configAccelerometer(boolean active)
    {
        if (active)
            enableAccelerationSensor(this);
        else
            disableAccelerationSensor();
    }

    /**
     * Reinitialize the camera
     */
    static void reinitCamera()
    {
        // center the camera
        _self._camera.setChaseEntity(null);
        _self._camera.setBoundsEnabled(false);
        _self._camera.setCenter(Configuration.getCameraWidth() / 2, Configuration.getCameraHeight() / 2);
        _self._camera.setZoomFactorDirect(1);
        _self._camera.setHUD(new HUD());
    }

    /*
     * ANDENGINE METHODS THAT ALE OVERLOADS
     */

    /**
     * Handle key presses by dispatching to the appropriate handler. Right now we only deal with the back button.
     * 
     * Note: user code should never call this
     * 
     * @param keyCode
     *            The key that was pressed
     * @param event
     *            The type of key event
     */
    @Override
    public boolean onKeyDown(final int keyCode, final KeyEvent event)
    {
        // if the back key was pressed down, draw the appropriate menu or
        // quit
        if ((keyCode == KeyEvent.KEYCODE_BACK) && (event.getAction() == KeyEvent.ACTION_DOWN)) {
            if (_menuManagerConfigured)
                return MenuManager.onBack();
        }
        // fall-back case for other key events
        return super.onKeyDown(keyCode, event);
    }

    /**
     * Main collision-detection routine: when a contact occurs, this dispatches appropriately so that the more important
     * entity manages the collision
     * 
     * Note: user code should never call this
     * 
     * @param contact
     *            They type of contact that occurred
     */
    @Override
    public void beginContact(Contact contact)
    {
        // get the two objects' userdata
        final Object a = contact.getFixtureA().getBody().getUserData();
        final Object b = contact.getFixtureB().getBody().getUserData();

        // NB: we can't actually do this work on the local thread; we need to
        // defer it and run it on the update thread. Otherwise, box2d might
        // crash.
        this.runOnUpdateThread(new Runnable()
        {
            @Override
            public void run()
            {
                // print a diagnostic message
                String msg1 = a == null ? "null" : a.toString();
                String msg2 = b == null ? "null" : b.toString();
                Debug.d("Collision: " + msg1 + " hit " + msg2);

                // we only do more if both are GFObjects
                if (!(a instanceof PhysicsSprite) || !(b instanceof PhysicsSprite))
                    return;

                // filter so that the one with the smaller type handles the
                // collision
                PhysicsSprite gfoA = (PhysicsSprite) a;
                PhysicsSprite gfoB = (PhysicsSprite) b;
                if (gfoA._psType._id > gfoB._psType._id)
                    gfoB.onCollide(gfoA);
                else
                    gfoA.onCollide(gfoB);
                // at this point, we should check for win/loss
            }
        });

    }

    /**
     * Unused collision detection routine
     * 
     * Note: user code should never call this
     */
    @Override
    public void endContact(Contact contact)
    {
    }

    /**
     * Unused collision detection routine
     * 
     * Note: user code should never call this
     */
    @Override
    public void postSolve(Contact contact, ContactImpulse impulse)
    {
    }

    /**
     * Unused collision detection routine
     * 
     * Note: user code should never call this
     */
    @Override
    public void preSolve(Contact contact, Manifold oldManifold)
    {
        // get the bodies
        Object a = contact.getFixtureA().getBody().getUserData();
        Object b = contact.getFixtureB().getBody().getUserData();

        // Convert to PhysicsSprites
        if (!(a instanceof PhysicsSprite) || !(b instanceof PhysicsSprite))
            return;
        PhysicsSprite gfoA = (PhysicsSprite) a;
        PhysicsSprite gfoB = (PhysicsSprite) b;

        // if the PhysicsSprites have the same passthrough ID, and it's not zero, then disable the contact
        if (gfoA._passThroughId != 0 && gfoA._passThroughId == gfoB._passThroughId) {
            contact.setEnabled(false);
            return;
        }
        
        // is either one-sided? If not, we're done
        PhysicsSprite onesided = null;
        PhysicsSprite other = null;
        if (gfoA._isOneSided > -1) {
            onesided = gfoA;
            other = gfoB;
        }
        else if (gfoB._isOneSided > -1) {
            onesided = gfoB;
            other = gfoA;
        }
        else {
            return;
        }

        // if we're here, see if we should be disabling a one-sided obstacle collision
        WorldManifold worldManiFold = contact.getWorldManifold();
        int numPoints = contact.getWorldManifold().getNumberOfContactPoints();
        for (int i = 0; i < numPoints; i++) {
            Vector2 vector2 = other._physBody.getLinearVelocityFromWorldPoint(worldManiFold.getPoints()[i]);
            // disable based on the value of _isOneSided and the vector between the entities
            if (onesided._isOneSided == 0 && vector2.y < 0)
                contact.setEnabled(false);
            else if (onesided._isOneSided == 2 && vector2.y > 0)
                contact.setEnabled(false);
            else if (onesided._isOneSided == 1 && vector2.x > 0)
                contact.setEnabled(false);
            else if (onesided._isOneSided == 3 && vector2.x < 0)
                contact.setEnabled(false);
        }
    }

    /**
     * whenever the tilt of the phone changes, this will be called automatically
     * 
     * Note: user code should never call this
     * 
     * @param info
     *            Information about the nature of the change in acceleration
     */
    @Override
    public void onAccelerationChanged(AccelerationData info)
    {
        Tilt.onAccelerationChanged(info);
    }

    /**
     * When the game is loaded, turn on vibration support
     * 
     * Note: user code should never call this
     */
    @Override
    public void onGameCreated()
    {
        mEngine.enableVibrator(this);
        super.onGameCreated();
    }

    /**
     * turn off _music when the game pauses. Without this, phone calls will suffer from _music still playing
     * 
     * Note: user code should never call this
     */
    @Override
    protected void onPause()
    {
        super.onPause();
        // if this gets called before onLoadEngine, tune won't be
        // configured, and the app could crash. To avoid problems,
        // we check if tune is not null first.
        if (Level._music != null && Level._music.isPlaying())
            Level._music.pause();
    }

    /**
     * When the activity is un-paused, restart the _music
     * 
     * Note: user code should never call this
     */
    @Override
    protected void onResume()
    {
        super.onResume();
        if (Level._music != null)
            Level._music.resume();
    }

    @Override
    public void onStop()
    {
        super.onStop();
        Splash.stopMusic();
    }

    /**
     * Configure the game engine
     * 
     * Note: user code should never call this
     */
    @Override
    public EngineOptions onCreateEngineOptions()
    {
        _self = this;
        // configure the camera.
        _camera = new SmoothCamera(0, 0, Configuration.getCameraWidth(), Configuration.getCameraHeight(),
                Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, 1f);

        // define the resolution
        RatioResolutionPolicy resolution = new RatioResolutionPolicy(Configuration.getCameraWidth(),
                Configuration.getCameraHeight());

        // next define the basic info: fullscreen, landscape, resolution,
        // camera
        EngineOptions eo = new EngineOptions(true, Configuration.getCameraOrientation(), resolution, _camera);

        // indicate that we may use sound and background _music
        eo.getAudioOptions().setNeedsMusic(true);
        eo.getAudioOptions().setNeedsSound(true);

        // turn on multitouch
        eo.getTouchOptions().setNeedsMultiTouch(true);

        // Return the engine options
        return eo;
    }

    /**
     * Load a scene. The _menuManager will either draw a splash screen, a chooser, or a playable level
     * 
     * Note: user code should never call this
     */
    @Override
    public Scene onCreateScene()
    {
        // make a menumanager if we don't have one yet
        if (!_menuManagerConfigured) {
            MenuManager.configure();
            _menuManagerConfigured = true;
            _helpScene = new HelpScene();
        }

        if (MenuManager._mode == MenuManager.Modes.SPLASH)
            return Splash.draw(MenuManager._menuFont);
        _self.configureLevel(1);
        return Level._current;
    }

    /**
     * When the scene is touched, this will run and forward to a handler for pokeable obstacles
     * 
     * Note: user code should never call this
     * 
     * TODO: We should just have a list of registered handlers, instead of forwarding to Obstacle...
     */
    @Override
    public boolean onSceneTouchEvent(final Scene pScene, final TouchEvent te)
    {
        return Obstacle.handleSceneTouch(pScene, te);
    }

    /**
     * AndEngine requires this, but we don't do anything when this changes...
     * 
     * Note: user code should never call this
     */
    @Override
    public void onAccelerationAccuracyChanged(AccelerationData arg0)
    {
    }

    /**
     * This method is required by AndEngine, and forwards to our method for naming the resources to load
     * 
     * Note: user code should never call this
     */
    @Override
    public void onCreateResources()
    {
        nameResources();
    }

    /*
     * PUBLIC METHODS THAT MUST BE OVERRIDDEN IN EVERY GAME
     */

    /**
     * Override this to indicate the names of the sound and picture files your game uses.
     */
    abstract public void nameResources();

    /**
     * To use the framework, you must override this to explain how to configure each level
     */
    abstract public void configureLevel(int whichLevel);

    /**
     * If you want to use help scenes, then you will need to override this method
     * 
     * NB: this isn't abstract, since you don't have to have help screens
     */
    public void configureHelpScene(int whichScene) { }

    /**
     * If you want to use trigger objects, you must override this to define what happens when the hero hits the obstacle
     * 
     * @param id
     *            The id that was assigned to the obstacle that was in the collision
     * @param whichLevel
     *            The _current level
     * @param o
     *            The obstacle involved in the collision
     * @param h
     *            The hero involved in the collision
     */
    abstract public void onHeroCollideTrigger(int id, int whichLevel, Obstacle o, Hero h);

    /**
     * If you want to use TouchTrigger objects, you must override this to define what happens when the object is
     * touched.
     * 
     * @param id
     *            The id that was assigned to the obstacle that was touched
     * @param whichLevel
     *            The _current level
     * @param o
     *            The obstacle that was touched
     */
    abstract public void onTouchTrigger(int id, int whichLevel, Obstacle o);

    /**
     * If you want to have timertriggers, then you must override this to define what happens when the timer expires
     * 
     * @param id
     *            The id that was assigned to the timer that exired
     * @param whichLevel
     *            The _current level
     */
    abstract public void onTimeTrigger(int id, int whichLevel);

    /**
     * If you want to have enemy timertriggers, then you must override this to define what happens when the timer
     * expires
     * 
     * @param id
     *            The id that was assigned to the timer that exired
     * @param whichLevel
     *            The _current level
     * @param e
     *            The enemy to modify
     */
    abstract public void onEnemyTimeTrigger(int id, int whichLevel, Enemy e);

    /**
     * If you want to have enemytriggers, then you must override this to define what happens when the enemy is defeated
     * 
     * @param id
     *            The id that was assigned to the enemy who was defeated
     * @param whichLevel
     *            The _current level
     * @param e
     *            The enemy that was defeated
     */
    abstract public void onEnemyDefeatTrigger(int id, int whichLevel, Enemy e);

    /**
     * If you want to have obstacletriggers, then you must override this to define what happens when an enemy hits the
     * obstacle
     * 
     * @param whichLevel
     *            The id that was assigned to the enemy who was defeated
     * @param o
     *            The obstacle involved in the collision
     * @param e
     *            The enemy involved in the collision
     */
    abstract public void onEnemyCollideTrigger(int id, int whichLevel, Obstacle oo, Enemy e);

    /**
     * If you want to have obstacletriggers, then you must override this to define what happens when a projectile hits
     * the obstacle
     * 
     * @param whichLevel
     *            The id that was assigned to the enemy who was defeated
     * @param o
     *            The obstacle involved in the collision
     * @param p
     *            The projectile involved in the collision
     */
    abstract public void onProjectileCollideTrigger(int id, int whichLevel, Obstacle o, Projectile p);

    /**
     * If you use TriggerControls, you must override this to define what happens when the control is pressed
     * 
     * @param id
     *            The id that was assigned to the Control
     * @param whichLevel
     *            The _current level
     */
    abstract public void onControlPressTrigger(int id, int whichLevel);

    /**
     * If you want to do something when the level ends (like record a high score), you will need to override this method
     * 
     * @param win
     *            true if the level was won, false otherwise
     * 
     */
    abstract public void levelCompleteTrigger(boolean win);

    /*
     * LIVE EDIT SUPPORT
     */

    /**
     * Launches the live editor so the developer can change various properties on the fly.
     * 
     * @param sprite
     *            The PhysicsSprite whose properties you want to change with the editor.
     */
    public static void launchLiveEditor(final PhysicsSprite sprite)
    {
        // it's possible to press a disabled and invisible body. Ignore any such touches.
        if (!sprite._physBody.isActive())
            return;

        // for regular clicks, we open a dialog on the UI thread
        _self.runOnUiThread(new Runnable()
        {
            @Override
            public void run()
            {
                // Build Dialog
                final Dialog dialog = new Dialog(_self);
                dialog.setContentView(R.layout.live_edit);
                dialog.setTitle("Live Edit");
                dialog.setCancelable(true);

                // Grab references to our fields
                final EditText editX = (EditText) dialog.findViewById(R.id.editX);
                final EditText editY = (EditText) dialog.findViewById(R.id.editY);
                final EditText editWidth = (EditText) dialog.findViewById(R.id.editWidth);
                final EditText editHeight = (EditText) dialog.findViewById(R.id.editHeight);
                final EditText editDensity = (EditText) dialog.findViewById(R.id.editDensity);
                final EditText editElasticity = (EditText) dialog.findViewById(R.id.editElasticity);
                final EditText editFriction = (EditText) dialog.findViewById(R.id.editFriction);

                // Update the fields with the current values
                editX.setText(sprite.getXPosition() + "");
                editY.setText(sprite.getYPosition() + "");
                editWidth.setText(sprite.getWidth() + "");
                editHeight.setText(sprite.getHeight() + "");
                editDensity.setText(sprite._physBody.getFixtureList().get(0).getDensity() + "");
                editElasticity.setText(sprite._physBody.getFixtureList().get(0).getRestitution() + "");
                editFriction.setText(sprite._physBody.getFixtureList().get(0).getFriction() + "");

                // When the dialog's update button is pressed, run this to update the sprite
                Button updateButton = (Button) dialog.findViewById(R.id.okButton);
                updateButton.setOnClickListener(new OnClickListener()
                {
                    @Override
                    public void onClick(View v)
                    {
                        // get values from the dialog
                        float newX = Float.parseFloat(editX.getText().toString());
                        float newY = Float.parseFloat(editY.getText().toString());
                        float newWidth = Float.parseFloat(editWidth.getText().toString());
                        float newHeight = Float.parseFloat(editHeight.getText().toString());
                        float newDensity = Float.parseFloat(editDensity.getText().toString());
                        float newElasticity = Float.parseFloat(editElasticity.getText().toString());
                        float newFriction = Float.parseFloat(editFriction.getText().toString());

                        // Set the sprite position and size
                        sprite._sprite.setX(newX);
                        sprite._sprite.setY(newY);
                        sprite._sprite.setWidth(newWidth);
                        sprite._sprite.setHeight(newHeight);

                        // build a new physics body. First get key parameters:
                        BodyType bt = sprite._physBody.getType();
                        boolean sensor = sprite._physBody.getFixtureList().get(0).isSensor();

                        // now remove the old body
                        sprite.deletePhysicsBody();

                        // Build new physics body based on shape
                        if (sprite._isCircle)
                            sprite.setCirclePhysics(newDensity, newElasticity, newFriction, bt, false);
                        else
                            sprite.setBoxPhysics(newDensity, newElasticity, newFriction, bt, false);

                        // update sensor status
                        sprite._physBody.getFixtureList().get(0).setSensor(sensor);

                        // return from dialog editor
                        _self.getEngine().start();
                        dialog.dismiss();
                    }
                });

                // When the dialog's cancel button is pressed, run this to dismiss the dialog without changing anything
                Button cancelButton = (Button) dialog.findViewById(R.id.cancelButton);
                cancelButton.setOnClickListener(new OnClickListener()
                {
                    @Override
                    public void onClick(View v)
                    {
                        _self.getEngine().start();
                        dialog.cancel();
                    }
                });
                _self.getEngine().stop();
                dialog.show();
            }
        });
    }
}